Improve auth token error formatting for easier copy-paste#5115
Improve auth token error formatting for easier copy-paste#5115jamesbroadhead wants to merge 9 commits intomainfrom
Conversation
When auth fails, the suggested `databricks auth login` command was printed on the same line as the error and config details, making it hard to copy. Split the message across multiple lines so the command is visually distinct and easy to select. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…formatting # Conflicts: # NEXT_CHANGELOG.md # cmd/auth/token_test.go
Approval status: pending
|
…command - The token-error format change in this branch landed only in acceptance/cmd/auth/token/output.txt; logout/stale-account-id-workspace-host and token/force-refresh-no-cache still asserted the old single-line message and fail CI. Regenerate both with the new multi-line format. - BuildLoginCommand now shell-quotes each argument before joining via libs/shellquote, so a profile or host containing spaces or shell metacharacters renders as a safe copy-paste command instead of a broken or unsafe one. BuildDescribeCommand quotes the profile too, for consistency. Co-authored-by: Isaac
helpfulError and BuildLoginCommand previously took only an OAuthArgument, which carries Host + AccountID + Profile but drops WorkspaceID. As a result, the suggested 'databricks auth login ...' for a token failure on a SPOG workspace hit either the wrong workspace or forced an unexpected interactive workspace selection. - Thread WorkspaceID through BuildLoginCommand and helpfulError. - Append --workspace-id to the rendered command when set, skipping empty values and the WorkspaceIDNone sentinel. - Plumb the workspace ID at the call sites: token.go (loadToken), root/auth.go (renderError), and the inline RewriteAuthError caller. - Add TestBuildLoginCommand_AppendsWorkspaceID covering profile path, unified host path, empty, and 'none' sentinel. - Regenerate the auth acceptance fixtures that exercise this path. Co-authored-by: Isaac
…formatting # Conflicts: # libs/auth/error.go # libs/auth/error_test.go
main removed AuthArguments.IsUnifiedHost in favor of DiscoveryURL-based routing (#5137). Update TestBuildLoginCommand_AppendsWorkspaceID's unified-host case to use a discovery URL containing /oidc/accounts/<id>/ so the test still exercises UnifiedOAuthArgument. Co-authored-by: Isaac
simonfaltum
left a comment
There was a problem hiding this comment.
Two small findings from review. The auth behavior mostly looks aligned with the existing code; these are the pieces I think should be cleaned up before merge.
|
|
||
| * `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API and `--workspace-id` to override the workspace routing identifier per call. A `?o=<workspace-id>` query parameter on the path (the SPOG URL convention used by the Databricks UI) is also recognized as a per-call workspace override, so URLs pasted from the browser route correctly. | ||
|
|
||
| * Improve `auth token` error formatting for easier copy-paste of login commands ([#4602](https://github.com/databricks/cli/pull/4602)) |
There was a problem hiding this comment.
This link still points to the replaced PR (#4602). Since this PR is the one that will merge, the release note should reference #5115; otherwise the shipped changelog sends readers to the abandoned PR.
| return | ||
| } | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, "", oauthArg)) | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, "", cfg.WorkspaceID, oauthArg)) |
There was a problem hiding this comment.
This non-profile branch now gets the safer command construction, but the cfg.Profile != "" branch just above still formats databricks auth login --profile %s by hand. That means profile names with spaces or shell metacharacters still produce broken/unsafe copy-paste commands in EnrichAuthError. Can the profile branch use BuildLoginCommand(ctx, cfg.Profile, cfg.WorkspaceID, nil) too, or otherwise quote consistently?
simonfaltum
left a comment
There was a problem hiding this comment.
Review summary
I ran a multi-agent review (Isaac + Cursor) on this reopened PR. Both reviewers converged on COMMENT with one important consistency gap.
The core auth token formatting change is correct and well-tested for the surface it touched, but the new BuildLoginCommand helper isn't used everywhere it should be — one sibling render path was missed.
Important:
writeReauthSteps'sAuthTypeDatabricksCliprofile branch still uses rawfmt.Fprintf(... "--profile %s", ...), bypassing the new helper. This is the most common 401 path. Result: it misses both--workspace-idand shell-quoting in exactly the workflow this PR is supposed to be normalizing.
Nits:
- Changelog links the superseded
#4602instead of#5115 RewriteAuthErrorlayout still single-paragraph vshelpfulError's blank-line-padded formBuildLoginCommanddoc-comment doesn't explain whyAccount/Workspace OAuthcases dropworkspaceID- Test gaps: no negative assertion for omission, no shell-quoting test
Question:
- Why include
--workspace-idon the profile path?auth login --profile fooalready picks up the profile's storedworkspace_id— worth a one-line note in the doc-comment to explain the intent (pin to the workspace the failing call resolved against?).
For full context I left these as inline comments below.
| return | ||
| } | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, "", oauthArg)) | ||
| fmt.Fprintf(b, "\n - Re-authenticate: %s", BuildLoginCommand(ctx, "", cfg.WorkspaceID, oauthArg)) |
There was a problem hiding this comment.
Important: the sibling profile branch (line 125) wasn't updated.
This branch correctly routes through BuildLoginCommand(ctx, "", cfg.WorkspaceID, oauthArg) now. But the if cfg.Profile != "" branch above (line 125 in the new file) still does fmt.Fprintf(b, "... --profile %s", cfg.Profile), bypassing the helper.
Result: a 401 on a profile-based databricks-cli call (the most common case) renders without --workspace-id and without shell-quoting, while this branch and the new helpfulError path do both. That makes the PR's own consistency claim only half-true — and on unified/SPOG hosts the suggested command can target the wrong workspace.
Fix: route the profile branch through BuildLoginCommand(ctx, cfg.Profile, cfg.WorkspaceID, nil). Add a test using a profile with WorkspaceID set, and ideally a profile name needing shell-quoting (e.g. with a space) to cover both improvements at once.
|
|
||
| * `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API and `--workspace-id` to override the workspace routing identifier per call. A `?o=<workspace-id>` query parameter on the path (the SPOG URL convention used by the Databricks UI) is also recognized as a per-call workspace override, so URLs pasted from the browser route correctly. | ||
|
|
||
| * Improve `auth token` error formatting for easier copy-paste of login commands ([#4602](https://github.com/databricks/cli/pull/4602)) |
There was a problem hiding this comment.
Nit: this links the superseded #4602, but this branch will merge as #5115. Worth pointing release-note readers at the actual merged PR.
| msg := `A new access token could not be retrieved because the refresh token is invalid. To reauthenticate, run the following command: | ||
| $ ` + BuildLoginCommand(ctx, profile, oauthArgument) | ||
| $ ` + BuildLoginCommand(ctx, profile, workspaceId, oauthArgument) |
There was a problem hiding this comment.
Nit: layout inconsistency.
RewriteAuthError keeps the legacy ...command:\n $ <cmd> shape, while the new helpfulError in cmd/auth/token.go uses a blank-line-padded layout (...command:\n\n $ <cmd>\n\n...). The PR description says the new format "matches the existing RewriteAuthError pattern," but they now diverge.
Compare acceptance/cmd/auth/token/force-refresh-invalid-refresh-token/output.txt (no blank lines around the command) with force-refresh-no-cache/output.txt (blank lines around the command).
Fix: unify on the new padded form here too — that's the actual goal of the PR. The acceptance test golden file would need a small update.
| // BuildLoginCommand builds the login command for the given OAuth argument or | ||
| // profile. Each argument is shell-quoted so the rendered command is safe to | ||
| // copy-paste even when host, profile, or account-id values contain spaces or | ||
| // shell metacharacters. | ||
| // | ||
| // workspaceID, when non-empty and not the WorkspaceIDNone sentinel, is | ||
| // emitted as --workspace-id for unified hosts so the suggested reauth | ||
| // targets the same workspace the failing call resolved against (the | ||
| // information the OAuthArgument otherwise drops). |
There was a problem hiding this comment.
Nit: the doc-comment explains the unified-host case for --workspace-id but not the AccountOAuthArgument/WorkspaceOAuthArgument cases below, which intentionally drop workspaceID. This is correct (account args target the account API; workspace args identify a workspace by host already), but the asymmetry isn't called out and is easy to break in a future change.
Fix: add one sentence — "For AccountOAuthArgument and WorkspaceOAuthArgument, workspaceID is intentionally ignored since those argument types either target the account API or already identify a workspace by host."
| func TestBuildLoginCommand_AppendsWorkspaceID(t *testing.T) { | ||
| ctx := t.Context() | ||
|
|
||
| t.Run("profile path emits --workspace-id when set", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "dev", "12345", nil) | ||
| assert.Equal(t, "databricks auth login --profile dev --workspace-id 12345", cmd) | ||
| }) | ||
|
|
||
| t.Run("profile path omits --workspace-id when empty", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "dev", "", nil) | ||
| assert.Equal(t, "databricks auth login --profile dev", cmd) | ||
| }) | ||
|
|
||
| t.Run("profile path omits --workspace-id for the 'none' sentinel", func(t *testing.T) { | ||
| cmd := BuildLoginCommand(ctx, "dev", WorkspaceIDNone, nil) | ||
| assert.Equal(t, "databricks auth login --profile dev", cmd) | ||
| }) | ||
|
|
||
| t.Run("unified host path emits --workspace-id when set", func(t *testing.T) { | ||
| oauthArg, err := AuthArguments{ | ||
| Host: "https://unified.cloud.databricks.com", | ||
| AccountID: "acc-123", | ||
| DiscoveryURL: "https://unified.cloud.databricks.com/oidc/accounts/acc-123/.well-known/oauth-authorization-server", | ||
| }.ToOAuthArgument() | ||
| require.NoError(t, err) | ||
|
|
||
| cmd := BuildLoginCommand(ctx, "", "ws-456", oauthArg) | ||
| assert.Contains(t, cmd, "--account-id acc-123") | ||
| assert.Contains(t, cmd, "--workspace-id ws-456") | ||
| }) | ||
| } |
There was a problem hiding this comment.
Nit: two coverage gaps in TestBuildLoginCommand_AppendsWorkspaceID:
- No negative assertion that
AccountOAuthArgument/WorkspaceOAuthArgumentpaths omit--workspace-ideven when one is provided. The production code does this intentionally; a regression test would lock that in. - No shell-quoting test. All the existing cases use safe profile names (
dev), soshellquote.BashArgis effectively untested. A profile name like"weird name"would cover the round-trip through the quoter.
Fix: two short table cases — one with WorkspaceOAuthArgument (e.g. classic https://adb-123.azuredatabricks.net) asserting --workspace-id is absent; one with profile = "weird name" asserting the output contains --profile 'weird name'. Both are one-liners.
| allArgs = append(allArgs, u2m.WithOAuthArgument(oauthArgument)) | ||
| persistentAuth, err := u2m.NewPersistentAuth(ctx, allArgs...) | ||
| if err != nil { | ||
| helpMsg := helpfulError(ctx, args.profileName, oauthArgument) | ||
| return nil, fmt.Errorf("%w. %s", err, helpMsg) | ||
| helpMsg := helpfulError(ctx, args.profileName, args.authArguments.WorkspaceID, oauthArgument) |
There was a problem hiding this comment.
Question: why include --workspace-id on the profile path?
When the user invokes databricks auth token --profile foo and we suggest databricks auth login --profile foo --workspace-id <id>, the --workspace-id is technically redundant — auth login --profile foo will already pick up the workspace_id stored in the profile.
Including it is meaningful only if (a) the profile's persisted workspace_id has drifted from the failing call's workspace_id, or (b) the user typed --workspace-id on the failing call and we want to preserve that intent. Both are plausible, but the doc-comment on BuildLoginCommand only motivates the unified-host case.
Not asking for a code change — just want to confirm this was deliberate. If yes, a one-line addendum to the doc-comment (e.g. "…also re-emitted on the profile path so the suggested reauth pins the same workspace_id the failing call resolved against") would close the loop.
Changes
Format the
auth tokenerror message across multiple lines so thesuggested
databricks auth logincommand is on its own line and easyto copy-paste.
Before:
After:
Why
When auth fails, the suggested login command was printed on the same line as the error and config details, making it difficult to select and copy. This follows the same multi-line pattern already used by
RewriteAuthErrorfor invalid refresh token errors.Tests
go test ./cmd/auth/ -run TestToken_loadToken)go test ./acceptance/... -run TestAccept/cmd/auth/token)